﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.IO;
using System.Linq;
using System.Net;
using System.Xml.Linq;
using System.Xml.Serialization;
using System.Xml;

namespace AEA.RSSFeedReaderLib
{
    /// <summary>
    /// Provides the ability to parse an XML RSS Feed and retrieve the data.
    /// </summary>
    [XmlRoot("Feeds")]
    [XmlInclude(typeof(ChannelFeed))]
    [XmlInclude(typeof(ItemFeed))]
    public class RSSFeed
    {
        #region Delegates
        /// <summary>
        /// Represents a method that will handle the DownLoadFeedCompleted event
        /// </summary>
        /// <param name="e"></param>
        public delegate void DownLoadFeedCompletedHandler(DownLoadFeedCompletedEventArgs e);

        /// <summary>
        /// Represents a method that will handle the DownLoadItemFeedCompleted event
        /// </summary>
        /// <param name="e"></param>
        public delegate void DownLoadItemFeedCompletedHandler(DownLoadItemFeedCompletedEventArgs e);
        #endregion

        #region Events
        /// <summary>
        /// Occurs when the feed has been completely downloaded.
        /// </summary>
        public event DownLoadFeedCompletedHandler DownLoadFeedCompleted;

        /// <summary>
        /// Occurs when the ItemFeeds have been completely downloaded.
        /// </summary>
        public event DownLoadItemFeedCompletedHandler DownLoadItemFeedCompleted;
        #endregion

        #region Member variables
        private ChannelFeedCollection _channels;

        private ChannelFeedCollection _channelToReload;

        private string DataFileName = "data.xml";

        private string ApplicationFolderName = "AEA Systems";

        private string AppDataFolderName = "RSS Feed Reader";
        #endregion

        #region Constructor
        /// <summary>
        /// Initialize a new instance of the RSSFeed class.
        /// </summary>
        public RSSFeed()
        {
            _channels = new ChannelFeedCollection();
            _channelToReload = new ChannelFeedCollection();
        }
        #endregion

        #region Properties
        /// <summary>
        /// Gets or sets the collection of ChannelFeeds
        /// </summary>
        public ChannelFeedCollection Channels
        {
            get
            {
                return this._channels;
            }
            set
            {
                this._channels = value;
            }
        }
        #endregion

        #region Class Methods

        #region Public Methods
        /// <summary>
        /// Download the feed from the specified Uri
        /// </summary>
        /// <param name="uri">The location of the feed to download from</param>
        public void DownLoadFeed(Uri uri)
        {
            WebClient webClient = new WebClient();
            webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(DownloadFeedCompleted);
            webClient.DownloadStringAsync(uri, uri);
        }

        /// <summary>
        /// Download the feed from the specified Uri
        /// </summary>
        /// <param name="channel">The channel to download the feeds for</param>
        public void DownLoadChannelItemFeeds(ChannelFeed channel)
        {
            if (!string.IsNullOrEmpty(channel.ChannelUri))
            {
                if (!IsChannelFeedInCollection(_channelToReload, channel))
                    _channelToReload.Add(channel);

                WebClient webClient = new WebClient();
                webClient.DownloadStringCompleted += new DownloadStringCompletedEventHandler(DownloadItemFeedCompleted);
                webClient.DownloadStringAsync(ConvertToUri(channel.ChannelUri), ConvertToUri(channel.ChannelUri));
            }
        }

        /// <summary>
        /// Persists the collection of ChannelFeeds to disk.
        /// </summary>
        public void Save()
        {
            // Return if there is nothing to save.
            if (this.Channels == null || this.Channels.Count == 0)
                return;

            // Temp folder where user data will be saved
            string tempFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                ApplicationFolderName);

            tempFolder = Path.Combine(tempFolder, AppDataFolderName);

            // Create the directory.
            Directory.CreateDirectory(tempFolder);

            // Create xml content file.
            XmlSerializer xmlSer = new XmlSerializer(typeof(RSSFeed));
            using (Stream stream = new FileStream(Path.Combine(tempFolder, DataFileName), FileMode.Create, FileAccess.Write, FileShare.None))
            {
                xmlSer.Serialize(stream, this);
            }

        }

        /// <summary>
        /// Load the list of ChannelFeeds from disk.
        /// </summary>
        public void Load()
        {
            try
            {
                // Temp folder where user data will be saved
                string tempFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData),
                    ApplicationFolderName);

                tempFolder = Path.Combine(tempFolder, AppDataFolderName);

                XmlSerializer xmlSer = new XmlSerializer(typeof(RSSFeed));
                using (Stream stream = new FileStream(Path.Combine(tempFolder, DataFileName), FileMode.Open, FileAccess.Read, FileShare.Read))
                {
                    RSSFeed rssFeed = (RSSFeed)xmlSer.Deserialize(stream);
                    stream.Close();

                    foreach (ChannelFeed channelFeed in rssFeed.Channels)
                        this.Channels.Add(channelFeed);
                }
            }
            catch
            {
                // Could not load the file so continue.
                return;
            }
        }

        /// <summary>
        /// Determines if a ChannelFeed in the collection all ready exists with the specified feed Uri.
        /// </summary>
        /// <param name="feed">The feed Uri to compare.</param>
        /// <returns>True if there is a ChannelFeed item with the specified feed Uri.</returns>
        public bool IsChannelFeedInCollection(string feed)
        {
            if (this.Channels.Count > 0)
            {
                foreach (ChannelFeed channelFeed in this.Channels)
                {
                    if (channelFeed.ChannelUri == feed)
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Determines if a ChannelFeed in the collection all ready exists.
        /// </summary>
        /// <param name="channelFeedCollection">The ChannelFeedCollection to base the comparison on.</param>
        /// <param name="channel">The ChannelFeed to look for in the ChannelFeedCollection.</param>
        /// <returns>True if there is a ChannelFeed item with the specified feed Uri.</returns>
        private bool IsChannelFeedInCollection(ChannelFeedCollection channelFeedCollection, ChannelFeed channel)
        {
            if (channelFeedCollection.Count > 0)
            {
                foreach (ChannelFeed channelFeed in channelFeedCollection)
                {
                    if (channel.Equals(channelFeed))
                    {
                        return true;
                    }
                }
            }

            return false;
        }

        /// <summary>
        /// Converts the specified string to System.Uri object.
        /// </summary>
        /// <param name="stringToConvert">The string to convert.</param>
        /// <returns>The string as a System.Uri object, or null if an error occured.</returns>
        public static Uri ConvertToUri(string stringToConvert)
        {
            try
            {
                Uri uri = new Uri(stringToConvert);
                return uri;
            }
            catch (Exception)
            {
                return null;
            }
        }

        /// <summary>
        /// Validates a Uri string.
        /// </summary>
        /// <param name="uri">The Uri to validate.</param>
        /// <returns>True if the Uri is valid, false otheriwse.</returns>
        public static bool IsValidUri(string uri)
        {
            string feedUrl = uri;

            Uri newUri = null;

            if (Uri.TryCreate(feedUrl, UriKind.Absolute, out newUri))
            {
                // If the URL references a local file, but the file does not
                // exist, then the URL is considered to be invalid.
                bool isLocalFile = newUri.IsLoopback;
                if (isLocalFile && !File.Exists(newUri.LocalPath))
                    newUri = null;
            }

            return newUri != null;
        }

        #endregion

        #region Private Methods
        /// <summary>
        /// Occurs when the feed has been downloaded
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DownloadFeedCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            int channelIndex = -1;
            Exception error = null;

            try
            {
                ProcessXML(e.Result, e.UserState.ToString(), false, out channelIndex);
            }
            catch (Exception ex)
            {
                error = ex;
            }
            finally
            {
                OnDownLoadFeedCompleted(new DownLoadFeedCompletedEventArgs(Channels, error));
            }
        }

        /// <summary>
        /// Occurs when the feed has been downloaded.
        /// </summary>
        /// <param name="sender"></param>
        /// <param name="e"></param>
        private void DownloadItemFeedCompleted(object sender, DownloadStringCompletedEventArgs e)
        {
            int channelIndex = -1;
            Exception error = null;

            try
            {
                ProcessXML(e.Result, e.UserState.ToString(), true, out channelIndex);
            }
            catch (Exception ex)
            {
                error = ex;
            }
            finally
            {
                if (channelIndex == -1)
                {
                    OnDownLoadItemFeedCompleted(new DownLoadItemFeedCompletedEventArgs(
                        new ChannelFeed(null, null, null, e.UserState.ToString(), null), 
                        null, error));
                }
                else
                {
                    OnDownLoadItemFeedCompleted(new DownLoadItemFeedCompletedEventArgs(_channelToReload[channelIndex],
                   _channelToReload[channelIndex].ItemFeeds, error));
                }
            }
        }

        /// <summary>
        /// Process the XML and retrieve the feeds.
        /// </summary>
        /// <param name="xml">The xml data.</param>
        /// <param name="xmlUri">The uri of the xml.</param>
        /// <param name="isItemFeedReload">True if reloading channel item feeds.</param>
        /// <param name="channelIndex">The index of the ChannelFeed to look for.</param>
        private void ProcessXML(string xml, string xmlUri, bool isItemFeedReload, out int channelIndex)
        {
            xml = CleanXML(xml);
            XDocument xmlRSSXDoc;
            ItemFeedCollection itemFeeds = new ItemFeedCollection();
            channelIndex = -1;

            // Parse the feeds into the ChannelFeed and ItemFeed class, then add to the relevant collections
            try
            {
                xmlRSSXDoc = XDocument.Parse(xml);

                var channel = from item in xmlRSSXDoc.Descendants("channel")
                              select new ChannelFeed
                              {
                                  ChannelTitle = item.Element("title").Value,
                                  ChannelLink = item.Element("link").Value,
                                  ChannelDescription = item.Element("description").Value,
                                  ChannelUri = xmlUri
                              };

                // For every item create an ItemFeed object 
                // and add to the object to
                // System.Collections.Generic.IEnumerable<ItemFeed> rssFeeds var
                var rssFeeds = from item in xmlRSSXDoc.Descendants("item")
                               select new ItemFeed
                               (
                                   item.Element("title").Value.Trim(),
                                   item.Element("description").Value,
                                   item.Element("link").Value
                               );

                // Get the objects in the System.Collections.Generic.IEnumerable<ItemFeed> rssFeeds var
                // and add to the ItemFeedCollection itemsFeeds
                foreach (ItemFeed itemFeed in rssFeeds)
                {
                    itemFeeds.Add(itemFeed);
                }

                ChannelFeed channelFeed = channel.FirstOrDefault();
                channelFeed.ItemFeeds = itemFeeds;

                if (!isItemFeedReload)
                {
                    // Add the ChannelFeed object channelFeed to the ChannelFeedCollection _channels                    
                    _channels.Add(channelFeed);
                }
                else
                {
                    // Reset the _channelToReload as we may have lost its original value by now
                    channelIndex = FindIndexOfChannel(channelFeed, _channelToReload);

                    if (channelIndex == -1)
                        throw new Exception(string.Format("Could not find index of channel this channel {0}.", channelFeed.ChannelUri));

                    _channelToReload[channelIndex].ItemFeeds = itemFeeds;

                    if (string.IsNullOrEmpty(_channelToReload[channelIndex].ChannelDescription))
                        _channelToReload[channelIndex].ChannelDescription = channelFeed.ChannelDescription;
                }
            }
            catch (XmlException ex)
            {
                throw new Exception(string.Format("The provided url ({0}) is not a valid RSS feed.", xmlUri), ex);
            }
            catch (Exception ex)
            {
                throw new Exception(string.Format("An unexpected error occured whilst trying to parse the feed ({0}).", xmlUri), ex);
            }
        }

        /// <summary>
        /// Gets the index of the ChannelFeed.
        /// </summary>
        /// <param name="channelToFind">The ChanneFeed to get the index of.</param>
        /// <param name="channelFeedCollection">The ChannelFeedCollection to search for the ChannelFeed in.</param>
        /// <returns>The index of the ChannelFeed or -1 if the ChannelFeed can not be found.</returns>
        private int FindIndexOfChannel(ChannelFeed channelToFind, ChannelFeedCollection channelFeedCollection)
        {
            foreach (ChannelFeed chan in channelFeedCollection)
            {
                if (chan.Equals(channelToFind))
                {
                    return channelFeedCollection.IndexOf(chan);
                }
            }

            return -1;
        }

        /// <summary>
        /// Remove namespaces from the xml.
        /// </summary>
        /// <param name="xml">The XML to remove the namespaces from.</param>
        /// <returns>The XML without the namespace prefixes.</returns>
        private string CleanXML(string xml)
        {
            // Remove name space declarations and characters
            xml = xml.Replace("dc:", "");
            xml = xml.Replace("slash:", "");
            xml = xml.Replace("â€“", "");
            xml = xml.Replace("â€¦", "");
           
            return xml;
        }

        /// <summary>
        /// Remove any ItemFeeds that have Reloading... as their title in case they
        /// are left over from the programme crashing.
        /// </summary>
        /// <param name="p">The Title of the ItemFeed to look for.</param>
        public void CleanFeedItems(string p)
        {
            Dictionary<ChannelFeed, List<ItemFeed>> itemFeedDict = new Dictionary<ChannelFeed, List<ItemFeed>>();
            List<ItemFeed> itemFeeds;
            if (this.Channels.Count() > 0)
            {
                foreach (ChannelFeed channelFeed in this.Channels)
                {
                    itemFeeds = new List<ItemFeed>();
                    foreach (ItemFeed itemFeed in channelFeed.ItemFeeds)
                    {
                        if (itemFeed.Title == p)
                        {
                            itemFeeds.Add(itemFeed);
                        }
                    }
                    if (itemFeeds.Count > 0)
                        itemFeedDict.Add(channelFeed, itemFeeds);
                }
            }

            foreach (KeyValuePair<ChannelFeed, List<ItemFeed>> keyPair in itemFeedDict)
            {
                foreach (ItemFeed itemFeedToRemove in keyPair.Value)
                {
                    this.Channels[FindIndexOfChannel(keyPair.Key, this.Channels)].ItemFeeds.Remove(itemFeedToRemove);
                }
            }
        }
        #endregion

        #region Protected Methods
        /// <summary>
        /// Fires the DownLoadFeedCompleted Event.
        /// </summary>
        /// <param name="e">The DownLoadFeedCompletedEventArgs arguments.</param>
        protected virtual void OnDownLoadFeedCompleted(DownLoadFeedCompletedEventArgs e)
        {
            if (DownLoadFeedCompleted != null)
            {
                DownLoadFeedCompleted(e);
            }
        }

        /// <summary>
        /// Fires the DownLoadFeedCompleted Event.
        /// </summary>
        /// <param name="e">The DownLoadFeedCompletedEventArgs arguments.</param>
        protected virtual void OnDownLoadItemFeedCompleted(DownLoadItemFeedCompletedEventArgs e)
        {
            if (DownLoadFeedCompleted != null)
            {
                DownLoadItemFeedCompleted(e);
            }
        }
        #endregion

        #endregion
    }
}